DLO-JZ Optimisation de l'apprentissage - Jour 2¶

Optimisation systĂšme d'une boucle d'apprentissage Resnet-152.

car

Objet du notebook¶

Le but de ce notebook est d'optimiser un code d'apprentissage d'un modÚle Resnet-50 sur Imagenet pour Jean Zay en implémentant :

  • TP 1 : l'optimisation du Dataloader
  • TP 2 : la distribution (Data Parallelism)

Les cellules dans ce notebook ne sont pas prĂ©vues pour ĂȘtre modifiĂ©es, sauf rares exceptions indiquĂ©es dans les commentaires. Les TP se feront en modifiant les codes dlojz2_X.py.

Les directives de modification seront marquées par l'étiquette TODO dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions/.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, janvier 2024


Environnement de calcul¶

Les fonctions python de gestion de queue SLURM développées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractÚres) pour vous différencier dans la queue SLURM pendant la formation.

In [1]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, comm_profiler, turbo_profiler, BatchNorm_view
MODULE = 'pytorch-gpu/py3/2.3.0'
image_size = 224
account = 'for@a100'
name = 'pseudo'   ## Pseudonyme Ă  choisir

Création d'un répertoire checkpoints/ si cela n'a pas déjà été fait.

In [2]:
!mkdir -p checkpoints



Gestion de la queue SLURM¶

Pour afficher vos jobs dans la queue SLURM :

In [3]:
display_slurm_queue(name)
 Done!

Remarque: cette fonction sera utilisĂ©e plusieurs fois dans ce notebook. Elle permet d'afficher la queue de maniĂšre dynamique, rafraichie toutes les 5 secondes. Elle ne s'arrĂȘte que lorsque la queue est vide. Si vous dĂ©sirez reprendre la main sur le notebook, il vous suffira d'arrĂȘter manuellement la cellule avec le bouton stop. Cela n'a bien sĂ»r aucun impact sur les jobs soumis.

Si vous voulez retirer TOUS vos jobs de la queue SLURM, décommenter et exécuter la cellule suivante :

In [4]:
#!scancel -u $USER

Si vous voulez retirer UN de vos jobs de la queue SLURM, décommenter, compléter et exécuter la cellule suivante :

In [5]:
#!scancel <jobid>

Différence entre deux scripts¶

Pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page HTML contenant un différentiel de fichiers texte.

In [6]:
s1 = "./dlojz2_2.py"
s2 = "./solutions/dlojz2_2.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


Garage - Mise à niveau¶

On fixe la taille d'image pour ce TP.

In [7]:
image_size = 224

On fixe le batch size optimal d'aprÚs les expériences du Jour 1.

In [8]:
bs_optim = 512

TP2_1: Optimisation du DataLoader¶

Dans ce TP, on utilisera le script dlojz2_1.py dans lequel le profiler PyTorch n'est pas implémenté. Ce script est identique à la solution du TP2_1.

Dans un premier temps, on va désactiver toutes les optimisations du DataLoader (version sous-optimisée). Ensuite, nous pourrons observer l'impact de chacune des optimisations possibles en les réintégrant une par une.

Découverte de turbo_profiler¶

Pour ce TP, nous avons implémenté un profiler maison léger turbo_profiler basé sur l'outil Chronometer pour visualiser le temps passé sur CPU (DataLoader) et sur GPU (le reste de l'itération). Ce profiler est moins précis mais cela nous permettra de désactiver le profiler PyTorch pour ne pas dégrader les performances et éviter de devoir ouvrir l'outil graphique TensorBoard à chaque fois pour visualiser les informations qui nous intéressent.

Version sous-optimisée¶

TODO : lancer l'exécution sur 1 GPU et 50 itérations (--test-nsteps 50) sans profiling pour passer un contrÎle technique qui servira de référence. Cela va prendre quelques minutes (~5min), vous pouvez passer à la suite sans attendre la fin de l'exécution.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [9]:
command = f'./dlojz2_1.py -b {bs_optim} --image-size {image_size} --test'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 249821
jobid = ['249821']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [10]:
#jobid = ['90764']
In [11]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            249821    gpu_p5   pseudo  cfor032  R       3:36      1 jean-zay-iam10

 Done!

Quizz¶

L'éxécution étant assez longue, un quizz vous attend : Quizz TP2_1

In [12]:
controle_technique(jobid)
Train throughput: 154.46 images/second
GPU throughput: 1152.99 images/second
epoch time: 8296.69 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.444063 sec (5.8%/94.1%) +/- 0.007393
loading step time average (IO + CPU to GPU transfer): 2.870636 sec +/- 0.630715

Click here to display the log file

TODO : visualiser la sortie de turbo_profiler

In [13]:
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
>>> Turbo Profiler >>> Training complete in 198.134031 s
No description has been provided for this image

Via le turbo profiler, on va également récupérer et stocker les performances obtenues dans une DataFrame dataloader_trials :

  • initialisation de la DataFrame :
In [14]:
import pandas as pd
dataloader_trials = pd.DataFrame({"jobid":pd.Series([],dtype=str),
                                  "num_workers":pd.Series([],dtype=int),
                                  "pin_memory":pd.Series([],dtype=str),
                                  "non_blocking":pd.Series([],dtype=str),
                                  "prefetch_factor":pd.Series([],dtype=int),
                                  "persistent_workers":pd.Series([],dtype=str),
                                  "drop_last":pd.Series([],dtype=str),
                                  "loading_time":pd.Series([],dtype=float),
                                  "CPU_memory_usage(GB)":pd.Series([],dtype=float)})
  • stockage du rĂ©sultat prĂ©cĂ©dent dans la DataFrame :
In [15]:
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
  • visualisation du contenu de la DataFrame :

In [16]:
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time")
Out[16]:
jobid num_workers pin_memory non_blocking prefetch_factor persistent_workers drop_last loading_time CPU_memory_usage(GB)
0 249821 0 False False 2 False False 2.870636 2.583473

Exploration des paramÚtres d'optimisation du DataLoader¶

L'objectif de ce TP est de réduire le temps passé sur CPU par le DataLoader.

Pour cette étude, on continue à lancer les exécutions sur 1 GPU et 16 itérations seulement (--test-nsteps 16) pour avancer plus rapidement.

Les différentes optimisations proposées par le DataLoader de PyTorch sont accessibles dans le script dlojz.py via les arguments :

  • --num-workers <num_workers> (dĂ©faut Ă  8)
  • --persistent-workers (dĂ©faut) ou --no-persistent-workers
  • --pin-memory (dĂ©faut) ou --no-pin-memory
  • --non-blocking (dĂ©faut) ou --no-non-blocking
  • --prefetch-factor <prefetch_factor> (dĂ©faut Ă  2)
  • --drop-last ou --no-drop-last (dĂ©faut)

TODO : faire varier ces différents paramÚtres et observer leurs effets grùce au profiler turbo_profiler. Pour comparer les différents essais, ceux-ci seront stockés dans la DataFrame dataloader_trials initialisée plus tÎt.

  1. Modifier un ou plusieurs paramÚtres du DataLoader et lancer l'exécution :

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [100]:
command = f'./dlojz2_1.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 16'

# paramÚtres d'entrée correspondant aux optimisations du DataLoader
command += ' --num-workers 8' 
command += ' --pin-memory'
command += ' --no-non-blocking'
command += ' --prefetch-factor 2'
command += ' --persistent-workers'
command += ' --no-drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250243
jobid = ['250243']
In [101]:
#jobid = ['2189183']
In [102]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250243    gpu_p5   pseudo  cfor032  R       0:34      1 jean-zay-iam19

 Done!
  1. Visualiser le retour du turbo profiler :
In [103]:
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
>>> Turbo Profiler >>> Training complete in 20.139863 s
No description has been provided for this image
  1. Stocker le nouveau résultat dans la DataFrame dataloader_trials :
In [104]:
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
  1. Visualiser et comparer l'ensemble des résultats :
In [105]:
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time").drop_duplicates()
Out[105]:
jobid num_workers pin_memory non_blocking prefetch_factor persistent_workers drop_last loading_time CPU_memory_usage(GB)
9 250021 16 True True 2 True False 0.000379 23.722820
8 249997 12 True True 2 True False 0.000393 18.223640
7 249992 8 True True 2 True True 0.000403 12.973003
6 249989 8 True True 2 False False 0.000568 12.972843
11 250243 8 True False 2 True False 0.013582 13.221825
2 249955 4 True False 2 False False 0.017398 7.722858
4 249973 4 True True 2 False False 0.073391 8.222889
10 250200 8 False True 2 True False 0.113451 2.014580
1 249951 4 False False 2 False False 0.189259 2.006294
3 249956 4 False True 2 False False 0.199624 2.010006
5 249979 1 True True 2 False False 1.973134 4.222897
0 249821 0 False False 2 False False 2.870636 2.583473
  1. Répéter les étapes 1. à 4. jusqu'à avoir trouvé des paramÚtres d'optimisation satisfaisants.

ContrÎle technique (version optimisée)¶

TODO : relancer l'exécution sur 1 GPU et 100 itérations (--test-nsteps 100) sans profiling pour passer un nouveau contrÎle technique, à comparer avec celui de référence.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [71]:
command = f'./dlojz2_1.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 100'

command += ' --num-workers 16' 
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --persistent-workers'
command += ' --no-drop-last'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250032
jobid = ['250032']
In [72]:
#jobid = ['2189222']
In [73]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250032    gpu_p5   pseudo  cfor032 CG       1:32      1 jean-zay-iam10

 Done!
In [74]:
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 69.433859 s
No description has been provided for this image
In [75]:
controle_technique(jobid)
Train throughput: 1151.19 images/second
GPU throughput: 1152.33 images/second
epoch time: 1113.22 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.444318 sec (6.4%/95.3%) +/- 0.034265
loading step time average (IO + CPU to GPU transfer): 0.000438 sec +/- 0.000167

Click here to display the log file

OPTIONNEL : Visualisation des traces profiler avec TensorBoard (version sous optimisée)¶

TODO : relancer le job en réactivant le profiler PyTorch dans le script dlojz2_1.py (revoir le TP1_4) afin de visualiser les traces sous TensorBoard, et les comparer avec la version optimisée étudiée dans le TP1_4.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [76]:
command = f'./dlojz2_1.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 8'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking'

n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 8 cpus per task
Submitted batch job 250044
jobid = ['250044']

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [77]:
#jobid = ['1732254']
In [78]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250044    gpu_p5   pseudo  cfor032 CG       1:15      1 jean-zay-iam10

 Done!

TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is224/ sous la forme d'un fichier .json:

In [79]:
!tree profiler/
profiler/
└── pseudo_250044_bs512_is224
    └── jean-zay-iam10_3802511.1729188592301545018.pt.trace.json

1 directory, 1 file

TODO : visualiser cette trace grĂące Ă  l'application TensorBoard.

IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).

Garage


Garage - Mise à niveau¶

In [80]:
image_size = 224
bs_optim = 512

TP2_2 : Distribution - Parallélisme de données¶

Voir la documentation de l'IDRIS.

TODO : dans le script dlojz2_2.py :

  • Importer les librairies liĂ©es Ă  la distribution et au Data Parallelism.

  • Configurer et initialiser l'environnement parallĂšle.

  • Associer le bon GPU allouĂ© au process actif.

  • Basculer le modĂšle en mode DistributedDataParallelism pour qu'il soit dupliquĂ© sur les diffĂ©rents GPU.

  • DĂ©finir les samplers distribuĂ©s train_sampler et val_sampler et les utiliser dans train_loader et val_loader respectivement. Attention, le shuffling devra ĂȘtre dĂ©lĂ©guĂ© aux samplers.

  • Au tout dĂ©but de la boucle d'apprentissage, indiquer au sampler l'epoch en cours afin d'obtenir un shuffling diffĂ©rent Ă  chaque epoch.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [81]:
command = f'./dlojz2_2.py -b {bs_optim} --image-size {image_size} --test --chkpt' 
n_gpu = 4
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 8 cpus per task
Submitted batch job 250065
jobid = ['250065']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [82]:
#jobid = ['2189271']
In [83]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250065    gpu_p5   pseudo  cfor032  R       1:22      1 jean-zay-iam19

 Done!

Quizz¶

L'éxécution étant assez longue, un quizz vous attend : Quizz TP2_2

In [84]:
controle_technique(jobid)
Train throughput: 4469.60 images/second
GPU throughput: 4473.26 images/second
epoch time: 286.84 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.457831 sec (8.8%/92.6%) +/- 0.030157
loading step time average (IO + CPU to GPU transfer): 0.000375 sec +/- 0.000086

Click here to display the log file

Communications¶

Découverte de comm_profiler¶

Pour ce TP, nous avons implémenté un profiler maison léger comm_profiler basé sur les traces de DEBUG de NCCL pour visualiser la quantité et le type de communications collectives échangées pendant une boucle d'apprentissage distribuée sur plusieurs GPU.

À noter : dans le script python dlojz2_2.py les variables de trace de DEBUG NCCL sont configurĂ©es comme suit :

if __name__ == '__main__':
    
    os.environ["NCCL_DEBUG"] = "INFO"
    os.environ["NCCL_DEBUG_SUBSYS"] = "INIT,COLL"
    # display info
    ...
In [85]:
comm_profiler(jobid, n_display=65)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

from pytorch documentation :

Each DDP process creates a local Reducer, which will take care of the gradients synchronization during the backward pass. To improve communication efficiency, the Reducer organizes parameter gradients into buckets, and reduces one bucket at a time. Bucket size can be configured by setting the bucket_cap_mb argument in DDP constructor. The mapping from parameter gradients to buckets is determined at the construction time, based on the bucket size limit and parameter sizes.

buckets

DDP inter-noeud¶

Nous avons utilisĂ© prĂ©cĂ©demment 4 GPU sur le mĂȘme nƓud de calcul. Les bus de communication intra-nƓud NVLink sont trĂšs rapide. Le scaling est quasi parfait.

Si nous utilisons 32 GPU en DDP avec 4 nƓuds de calcul et donc des communications sur le rĂ©seau d'interconnexion des nƓuds nous obtenons le rĂ©sultat suivant.

DDP 32 GPU

Ce test n'est pas faisable pendant le TP par chacun d'entre vous, pour des raisons évidentes d'accÚs aux ressources. Veuillez vous reporter au résultat fourni ici.

Commentaires

BatchNorm Layer & SyncBatchNorm Layer¶

Rappel :

Pendant l'apprentissage, la couche normalise ses sorties en utilisant la moyenne et l'écart type du batch d'entrée. Plus exactement, la couche retourne (batch - mean(batch)) / (var(batch) + epsilon) * weight + bias , avec :

  • epsilon, une petite constante pour Ă©viter la division par 0,
  • weight, un facteur appris (entraĂźnĂ©) avec un calcul de gradient lors de la backpropagation et qui est initialisĂ© Ă  1,
  • bias, un facteur appris (entraĂźnĂ©) avec un calcul de gradient lors de la backpropagation et qui est initialisĂ© Ă  0.

Pendant l'inférence ou la validation, la couche normalise ses sorties en utilisant en plus des weight et bias entraßnés, les facteurs running_mean et running_var : (batch - running_mean) / (running_var + epsilon) * weight + bias.

running_mean et running_var sont des facteurs non entraßnés, mais qui sont mis à jour à chaque itération de batch lors de l'apprentissage, selon la méthode suivante :

  • running_mean = running_mean * momentum + mean(batch) * (1 - momentum)
  • running_var = running_var * momentum + var(batch) * (1 - momentum)
In [86]:
import torchvision.models as models
model = models.resnet152()
In [87]:
BatchNorm_view(jobid, model)
No description has been provided for this image

SyncBatchNorm layer¶

Voir la documentation PyTorch.

TODO : dans le script dlojz2_2.py :

  • Juste avant la bascule du modĂšle en mode DistributedDataParallelism, transformer les couches BatchNorm du modĂšle en couches SyncBatchNorm.

Soumission du job. Attention vous sollicitez les noeuds de calcul Ă  ce moment-lĂ .

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [88]:
command = f'./dlojz2_2.py -b {bs_optim} --image-size {image_size} --test --chkpt'
n_gpu = 4
jobid_sync = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                    account=account, time_max='00:10:00')
print(f'jobid_sync = {jobid_sync}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 8 cpus per task
Submitted batch job 250096
jobid_sync = ['250096']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [89]:
#jobid_sync = ['2189317']
In [90]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            250096    gpu_p5   pseudo  cfor032  R       1:19      1 jean-zay-iam19

 Done!
In [91]:
controle_technique(jobid_sync)
Train throughput: 3465.92 images/second
GPU throughput: 3468.12 images/second
epoch time: 369.90 seconds
-----------
training step time average (fwd/bkwd on GPU): 0.590521 sec (50.4%/49.7%) +/- 0.004875
loading step time average (IO + CPU to GPU transfer): 0.000375 sec +/- 0.000148

Click here to display the log file

In [92]:
BatchNorm_view(jobid + jobid_sync, model, labels=['BN Layer', 'SyncBN Layers'])
No description has been provided for this image

Communications¶

In [93]:
comm_profiler(jobid_sync, n_display=100)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Garage